package org.activequant.core.domainmodel.marketscanner;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import org.activequant.core.types.TimeStamp;
import org.activequant.util.exceptions.ValueNotFoundException;
import org.activequant.util.tools.ArrayUtils;

/**
* Series base class which handles a set of elements and the according specification.<br>
* Each element within the collection is unique, cannot be stored twice and belongs<br>
* to this collection.<br/>
* An abstract class, ArrayList subclass. Holds the following associated variables:
* <ul>
* <li>id(Long)</li>
* <li>marketScanSeriesSpecification(MarketScanSeriesSpecification)</li>
* <li>inherited list of elements (each of them a MarketDataEntity subclass)</li>
* </ul>
*  @author Dimitar Misev
*/
public abstract class ScanDataTimeSeries<T extends ScanDataEntity<T>> extends ArrayList<T> {	
	/**
	 * protected Long id;
	 */
	protected Long id;
	/**
	 * protected MarketScanSeriesSpecification marketScanSeriesSpecification;
	 */
	protected MarketScanSeriesSpecification marketScanSeriesSpecification;
	
	public abstract ScanDataTimeSeries<T> clone();
	/**
	 * empty constructor
	 */
	protected ScanDataTimeSeries() {
		
	}
	/**
	 * constructs a ScanDataTimeSeries using the given marketScanSeriesSpecification(MarketScanSeriesSpecification) to set its associated marketScanSeriesSpecification(MarketScanSeriesSpecification)
	 * @param marketScanSeriesSpecification
	 */
	protected ScanDataTimeSeries(MarketScanSeriesSpecification marketScanSeriesSpecification) {
		this.marketScanSeriesSpecification = marketScanSeriesSpecification;
	}
	/**
	 * constructs a ScanDataTimeSeries(an ArrayList subclass) using the given seriesData(List&lt;T&gt;) as its elements
	 * @param seriesData
	 */
	protected ScanDataTimeSeries(List<T> seriesData) {
		super(seriesData);
	}
	
	/**
	 * returns the associated id(Long)
	 * @return the id
	 */    
	public Long getId() {
		return id;
	}
	
	/**
	 * sets the associated id(Long) with the given id(Long)
	 * @param id the id to set
	 */
	public void setId(Long id) {
		this.id = id;
	}
	
	/**
	 * returns the scanCode(String) of the scanCriteria(ScanCriteria) of the associated marketScanSeriesSpecification(MarketScanSeriesSpecification)<br/>
	 * @return
	 */
	public String getScanCode() {
		return getScanCriteria().getScanCode();
	}
	
	//############### Specification ###############
	
	/**
	 * Returns <code>marketScanSeriesSpecification.toString()</code><br/>
	 * returns the name of this series
	 * @return the name
	 */
	public String getName() {
		return marketScanSeriesSpecification.toString();
	}
	
	/**
	 * returns whether the associated marketScanSeriesSpecification(MarketScanSeriesSpecification) is not <code>null</code><br/>
	 * returns true if an instrument specification is associated
	 * @return
	 */
	public boolean hasName() {
		return marketScanSeriesSpecification != null;
	}
	
	/**
	 * applies a given scanCriteria to all elements of this list<br/>
	 * <strong>1.</strong> sets the scanCriteria(ScanCriteria) of the associated marketScanSeriesSpecification(MarketScanSeriesSpecification) with the given scanCriteria(ScanCriteria)<br/>
	 * <strong>2.</strong> sets the scanCriteria(ScanCriteria) of each of the elements (MarketDataEntity subclasses) of this ScanDataTimeSeries(an ArrayList subclass) with the given scanCriteria(ScanCriteria)
	 * @param scanCriteria
	 */
	public void applyScanCriteria(ScanCriteria scanCriteria) {
		assert(scanCriteria != null);
		marketScanSeriesSpecification.setScanCriteria(scanCriteria);
		for(T object : this) {
			object.setScanCriteria(scanCriteria);
		}
	}
	
	/**
	 * applies a given marketScan series specification to the series itself and all elements<br/>
	 * <strong>1.</strong> sets the associated marketScanSeriesSpecification(MarketScanSeriesSpecification) with the given marketScanSeriesSpecification(MarketScanSeriesSpecification)<br/>
	 * <strong>2.</strong> sets the scanCriteria(ScanCriteria) of each of the elements (ScanDataEntity subclasses) of this ScanDataTimeSeries(an ArrayList subclass) 
	 * with the scanCriteria(ScanCriteria) of the given marketScanSeriesSpecification(MarketScanSeriesSpecification)
	 * @param marketScanSeriesSpecification
	 */
	public void applySeriesSpecification(MarketScanSeriesSpecification marketScanSeriesSpecification) {
		assert(marketScanSeriesSpecification != null);
		this.marketScanSeriesSpecification = marketScanSeriesSpecification;
		for(T object : this) {
			object.setScanCriteria(marketScanSeriesSpecification.getScanCriteria());
		}
	}
	/**
	 * returns the associated marketScanSeriesSpecification(MarketScanSeriesSpecification)
	 * @return
	 */
	public MarketScanSeriesSpecification getMarketScanSeriesSpecification() {
		return marketScanSeriesSpecification;
	}
	/**
	 * sets the associated marketScanSeriesSpecification(MarketScanSeriesSpecification) with the given marketScanSeriesSpecification(MarketScanSeriesSpecification)
	 * @param marketScanSeriesSpecification
	 */
	public void setMarketScanSeriesSpecification(MarketScanSeriesSpecification marketScanSeriesSpecification) {
		this.marketScanSeriesSpecification = marketScanSeriesSpecification;
	}
	/**
	 * returns the scanCriteria(ScanCriteria) of the associated marketScanSeriesSpecification(MarketScanSeriesSpecification)
	 * @return
	 */
	public ScanCriteria getScanCriteria() {
		assert(marketScanSeriesSpecification != null);
		return marketScanSeriesSpecification.getScanCriteria();
	}
	
	//############### List operations ###############
	
	/**
	 * generic method which allows to set the <T> entities directly<br/>
	 * sets the elements of this ScanDataTimeSeries(an ArrayList subclass) with the given entities(T...)
	 * @param entities
	 */
	public void setEntities(T... entities) {
		this.clear();
		this.addAll(ArrayUtils.asList(entities));
	}
	
	/**
	 * returns a cloned sub-list
	 * @param start
	 * @param end
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public ScanDataTimeSeries<T> clonedSubList(int start, int end) {
		assert(start >= 0 && end <= size() && start < end) : "start < 0 || end > size() || start > end";
		ScanDataTimeSeries<T> clone = (ScanDataTimeSeries<T>) clone();
		java.util.List<T> subList = super.subList(start, end);
		clone.clear();
		clone.addAll(subList);
		return clone;
	}
	
	/**
	 * returns the first element of this ScanDataTimeSeries(an ArrayList subclass)
	 * @return
	 */
	public T firstElement() {
		assert(!this.isEmpty());
		return get(0);
	}
	/**
	 * returns the last element of this ScanDataTimeSeries(ArrayList subclass)
	 * @return
	 */
	public T lastElement() {
		assert(!this.isEmpty());
		return this.get(size()-1);
	}
	
	
	/**
	 * returns the position of the given date within the array<br/>
	 * iterates the elements of this ScanDataTimeSeries(ArrayList subclass) and returns the position of the element whose timeStamp(TimeStamp) matches the given date(TimeStamp)
	 * @param date
	 * @return
	 * @throws ValueNotFoundException
	 */
	public int getTimeStampPosition(TimeStamp date) throws ValueNotFoundException {
		ListIterator<T> iter = this.listIterator();
		while (iter.hasNext()) {
			if (iter.next().getTimeStamp().equals(date))
				return iter.previousIndex();
		}
		throw new ValueNotFoundException("Cannot find value for date '" + date + "'.");
	}
	/**
	 * returns a TimeStamp[] containing the TimeStamp of every one of the elements of this ScanDataTimeSeries(ArrayList subclass)
	 * @return
	 */
	public TimeStamp[] getTimeStamps() {
	    TimeStamp[] dates = new TimeStamp[size()];
	    int idx = 0;
	    for (T element : this) {
	    	dates[idx++] = element.getTimeStamp();
	    }
	    return dates;
	}
	/**
	 * sets the TimeStamps of the elements of this ScanDataTimeSeries(ArrayList subclass) with the given dates(TimeStamp[])
	 * @param dates
	 */
	public void setTimeStamps(TimeStamp[] dates) {
	    int idx = 0;
	    for(T element : this) {
	    	element.setTimeStamp(dates[idx++]);
	    }
	}
	/**
	 * returns the element whose timeStamp(TimeStamp) matches the given date(TimeStamp)
	 * @param date
	 * @return
	 * @throws ValueNotFoundException
	 */
	public T getByTimeStamp(TimeStamp date) throws ValueNotFoundException {
		return get(getTimeStampPosition(date));
	}
	/**
	 * removes the element whose timeStamp(TimeStamp) matches the given date(TimeStamp)
	 * @param date
	 * @throws ValueNotFoundException
	 */
	public void removeByTimeStamp(TimeStamp date) throws ValueNotFoundException {
		remove(getByTimeStamp(date));
	}
	
	/**
	 * returns whether there is an element whose timeStamp(TimeStamp) matches the given date(TimeStamp)
	 * @param date
	 * @return
	 */
	public boolean containsTimeStamp(TimeStamp date) {
		for(TimeStamp checkDate: getTimeStamps()) {
			if(checkDate.isEqual(date))
				return true;
		}
		return false;
	}	
	
	/**
	 * method to fetch a subset / timeframe of this series.<br/>
	 * returns the elements whose TimeStamps are within(inclusive) the given start-end(TimeStamp) interval
	 * @param start
	 * @param end
	 * @return
	 */
	public List<T> getTimeFrame(TimeStamp start, TimeStamp end) {
		if(!start.isBefore(end)) {
			throw new IllegalArgumentException("start must be before end: start=" + start + ", end=" + end);
		}
		List<T> list = clone();
		list.clear();
	    for (T t : this) {
	    	if ((t.getTimeStamp().isAfter(start) && t.getTimeStamp().isBefore(end))
	        	|| t.getTimeStamp().equals(start) || t.getTimeStamp().equals(end) ) {
	        	list.add(t);
	        }
	    }
	    return list;
	}
	
	/**
	 * returns a sublist for the given positions.
	 * @param start
	 * @param end
	 * @return
	 */
	public List<T> subList(int start, int end) {
		// note: endPos > startPos 
		assert(start >= 0 && end <= size() && start < end) : "start < 0 || end > size() || start > end";
		List<T> list = clone();
		list.clear();
		List<T> subList = super.subList(start, end);
		list.addAll(subList);
		return list;
	}
	
	/**
	 * returns a sublist for the given period of time.
	 * @param start
	 * @param end
	 * @return
	 * @throws ValueNotFoundException
	 */
	public List<T> subList(TimeStamp start, TimeStamp end) throws ValueNotFoundException {
		// note: startPos > endPos
		assert(start.isBefore(end));
		int startPos = getTimeStampPosition(start);
		int endPos = getTimeStampPosition(end);
		return subList(endPos, startPos);
	}
}